/**
 * 在文件系统保存保存点
 * 一共有两个文件,一个是索引文件,另一个是数据文件
 */

/**
 * 索引文件中,索引项的数据结构
 * @typedef Index
 * @property {String} time   时间戳
 * @property {String} desc   用户输入的索引说明
 * @property {number} pos    起始位置
 * @property {number} length 长度
 */

const { confirmDir, formatDate, promisify, info, errorInfo } = require('../utils.js');
const path = require('path');
const fs = require('fs');
const zlib = require('zlib');

/** 
 *  保存点文件由索引文件和数据文件组成
 *  索引文件:保存数据文件的索引信息,scheme,settings
 *  数据文件为连续的二进制文件
 */
class SavePointFile {
    /**
     * 确保dir已经创建,打开保存点文件,读入索引
     * @param {String} dir 
     *    保存点文件的路径
     * @param {String} name
     *    保存文件名 
     */
    constructor(dir, name) {
        confirmDir(dir);
        this.dir = dir;
        this.name = name;
        //json文件
        this.indexFile = path.join(dir, name + '.index.json');
        //二进制文件
        this.dataFile = path.join(dir, name + '.data');
    }

    /**
     * @param {String} scheme
     *    方案体系标识
     */
    setScheme(scheme) {
        this.scheme = scheme;
    }

    /**
     * 索引文件中还要保存配置
     * @param {{}} settings 
     */
    setSettings(settings) {
        this.settings = settings;
    }

    setProperties(properties) {
        this.properties = Object.assign(this.properties || {}, properties);
    }

    async init() {
        const [data, err] = await promisify(resolve => {
            fs.readFile(this.indexFile, 'utf-8', (err, data) => {
                resolve(data, err);
            })
        });

        if (data) {
            const json = eval('(' + data + ')');
            this.scheme = json.scheme;
            this.indexes = json.indexes;
            this.settings = json.settings;
            this.properties = json.properties;
        } else {
            this.indexes = {};
        }
    }

    async _close(fd) {
        const [, closeErr] = await promisify(resolve => {
            fs.close(fd, err => {
                resolve(null, err);
            })
        });

        if (closeErr) errorInfo(closeErr);
    }

    /**
     * 从数据文件中读取保存点数据,这是一个内部方法,参数为时间戳数组,返回的结果数组没有解压缩
     */
    async _readDataFileFor(...times) {
        const [fd, openErr] = await promisify(resolve => {
            fs.open(this.dataFile, "r", (err, fd) => {
                resolve(fd, err);
            });
        });

        if (openErr) throw openErr;


        const bufs = [];
        for (let i = 0, l = times.length; i < l; i++) {
            const time = times[i];
            const index = this.indexes[time];
            if (index) {
                const [buf, err] = await promisify(resolve => {
                    const buf = Buffer.alloc(index.length);
                    fs.read(fd, buf, 0, index.length, index.pos, (err, _buf) => {
                        resolve(buf, err);
                    });
                });

                if (err) {
                    await this._close(fd);
                    throw err;
                }
                bufs.push(buf);
            }
        }

        await this._close(fd);

        return bufs;
    }

    /**
     * 从数据文件中读取一个时间戳对应的数据,解压并返回字符串
     * @param {String} time 
     */
    async readSavePoint(time) {
        const res = await this._readDataFileFor(time);
        if (res[0]) {
            let buf = res[0], err;
            [buf, err] = await promisify(resolve => zlib.gunzip(buf, (err, resBub) => resolve(resBub, err)));
            if (err) {
                throw err;
            }
            return buf.toString('utf8');
        }
    }

    /**
     * savepoint时间和desc写入索引文件,bin写入数据文件
     * 注意写时,不能和已有时间点冲突
     * @param {String} time
     *     保存点时间戳 
     * @param {String} desc
     *     保存点描述 
     * @param {String} data
     *     保存点数据 
     */
    async writeSavePoint(time, desc, data) {
        if (this.indexes[time]) {
            throw new Error(`savepoint文件:${this.dir}/${this.name}的保存点${time}重复`);
        }

        const [size, statErr] = await promisify(resolve => {
            fs.stat(this.dataFile, (err, status) => {
                resolve(err ? null : status.size, err);
            })
        });

        const pos = statErr ? 0 : size;

        /** 
         * 压缩数据 
         */
        let buf = Buffer.from(data, 'utf8'), zipErr;
        [buf, zipErr] = await promisify(reslove => zlib.gzip(buf, (err, resBuf) => reslove(resBuf, err)));
        if (zipErr) throw zipErr;

        /**
         * 修改对象内索引缓存
         */
        this.indexes[time] = {
            time, desc, pos,
            length: buf.length
        }

        /**
         * 写索引文件
         */
        await this.writeIndexFile();

        /**
         * 写数据文件
         */
        const [, wErr] = await promisify(resolve => {
            //appendFile也能自动创建文件
            fs.appendFile(this.dataFile, buf, null, err => {
                resolve(null, err);
            })
        });

        if (wErr) throw wErr;
    }

    /**
     * 保存索引文件,每次更新都是覆盖
     */
    async writeIndexFile() {
        if (!this.scheme) throw new Error(`savepoint文件:${this.dir}/${this.name}scheme为空`);

        const indexFileContent = JSON.stringify(
            {
                scheme: this.scheme,
                indexes: this.indexes,
                settings: this.settings,
                properties: this.properties
            },
            null, 2);
        const [, writeErr] = await promisify(resolve => {
            fs.writeFile(this.indexFile, indexFileContent, 'utf-8', err => {
                resolve(null, err);
            });
        });

        if (writeErr) throw writeErr;
    }

    /**
     * 删除某个时间戳的保存点,简单一点实现就是在索引文件上做个标识
     * @param {String[]} times 
     */
    async deleteSavePoint(times) {
        times = [].concat(times);
        if (times.length) {
            times.forEach(time => {
                let index = this.indexes[time];
                //标识删除
                index && (index.deleted = true);
            });
            await this.writeIndexFile();
            return true;
        }
        return false;
    }

    /**
     * 清除索引文件上已经删除的保存点,重写数据文件
     */
    async clearDeleted() {
        const indexes = {}, times = Object.keys(this.indexes);
        //默认排序
        times.sort();
        let pos = 0;
        let allBuf;
        const bufs = await this._readDataFileFor(...times);
        for (let i = 0, l = times.length; i < l; i++) {
            const time = times[i];
            if (!this.indexes[time].deleted) {
                /**
                * @type {Buffer}
                */
                const buf = bufs[i];
                indexes[time] = {
                    time,
                    desc: this.indexes[time].desc,
                    pos,
                    length: buf.length
                };
                pos += buf.length;
                //合并allBUf
                allBuf = allBuf ? Buffer.concat([allBuf, buf]) : buf;
            }
        }
        /**
         * 重置对象内缓存的indexes(干净的索引文件,不再有删除标识),并写入
         */
        this.indexes = indexes;
        await this.writeIndexFile();

        /**
         * 写入数据文件
         */
        const [, err] = await promisify(resolve => {
            fs.writeFile(this.dataFile, allBuf, null, err => {
                resolve(null, err);
            })
        });

        if (err) throw err;
    }
}

async function openSavePointFile(dir, name) {
    const savePointFile = new SavePointFile(dir, name);
    await savePointFile.init();
    return savePointFile;
}


function getSavePointFilePath(targetRelaPath) {
    //把requie写在方法中,是因为这个文件需要脱离vscode插件端进行实验
    const { savePointDir } = require('../base.js');
    const savePointFilePath = path.join(savePointDir, targetRelaPath);

    const dir = path.dirname(savePointFilePath);
    //文件名
    const fileName = path.relative(dir, savePointFilePath);
    //文件名去掉扩展名
    const targetName = fileName.substring(0, fileName.indexOf('.'));

    return [dir, targetName];
}


const spfCache = {};

/**
 * 对某个目标文件打开savePointFile,这个打开不需要关闭
 * 此方法目前已经对spf对象进行了缓存
 * @param {*} targetRelaPath 
 * @returns {SavePointFile}
 */
async function openSavePointFileForFile(targetRelaPath) {
    let spf = spfCache[targetRelaPath];
    if (!spf) {
        const [dir, target] = getSavePointFilePath(targetRelaPath);
        info('savePointFilePath:', dir, target);
        //confirmDir参数必须为一个文件路径,可以不存在
        confirmDir(path.join(dir, target));

        spf = await openSavePointFile(dir, target);
        await spf.init();
        spfCache[targetRelaPath] = spf;
    }

    return spf;
}

/**
 * 
 * @param {{context:*,registerService:function}} serviceHandler 
 */
function contributeServicesForSavePoint(serviceHandler) {
    const getRelaPathFromWebView = serviceHandler.context.getRelaPathFromWebView;
    const panelHandler = serviceHandler.context.panelHandler;
    const vscode = require('vscode');
    const w = vscode.window;
    const getRelaPathFromWebViewNotDemo = function (reply) {
        const targetRelaPath = getRelaPathFromWebView(reply);
        if (typeof targetRelaPath !== 'string') {
            /**
             * 当reply中不存在targetRelaPath时,getRelaPathFromWebView方法会赋予默认值Symbol.for('demo')
             */
            w.showInformationMessage('mxdev演示文件有保存点!');
        } else {
            return targetRelaPath;
        }
    }

    /**
     * 即时保存点,config的每次改变,都会推送保存点对象到插件端,这样在用户关闭设计页面或设计页面崩溃的时候,
     * 可以自动对用户的最新设计进行存档
     * @todo :这个将来要对同一个文件支持六个临时存档(+当前指针),以支持历史记录的前进和回退功能
     */
    const immediatelySavePoints = (function () {
        /**
         * targetRelaPath-->[dirty,oldSerializedRootedConfig]
         */
        const icache = {};
        return {
            /**
             * 后面调用
             */
            set(targetRelaPath, serializedRootedConfig) {
                let a = icache[targetRelaPath];
                if (!a) {
                    icache[targetRelaPath] = [true, serializedRootedConfig];
                } else {
                    //序列化后的字符串比较
                    a[0] = a[1] !== serializedRootedConfig;
                    a[1] = serializedRootedConfig;
                }
            },

            /**
             * 加载savepoint时调用
             */
            init(targetRelaPath, serializedRootedConfig) {
                icache[targetRelaPath] = [false, serializedRootedConfig];
            },

            /**
             * 报损后清空dirty状态
             */
            clearDirty(targetRelaPath) {
                let a = icache[targetRelaPath];
                a && (a[0] = false);
            },

            get(targetRelaPath) {
                let a = icache[targetRelaPath];
                return a ? a : [false, null];
            },
        }
    })();

    /**
     * 这里都是文件操作,不涉及ui的稳定性,所以都注册为服务的同步方法
     * 客户端在调用时应避免同时对同一个文件进行操作
     */
    const spfService = {
        /**
         * 保存settings
         * @param {*} settings 
         * @param {*} reply 
         */
        async saveDevSettings(settings, reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                /**
                 * @type {SavePointFile}
                 */
                const spf = await openSavePointFileForFile(targetRelaPath);
                spf.setScheme(settings.scheme);
                spf.setSettings(settings);
                //只写索引文件
                await spf.writeIndexFile();
            }

            reply();
        },

        /**
         * 读取settings
         * @param {*} reply 
         */
        async readDevSettings(reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                const spf = await openSavePointFileForFile(targetRelaPath);
                reply(spf.settings || {});
            }

            reply();
        },


        /**
        * 保存properties
        * @param {*} properties 
        * @param {*} reply 
        */
        async saveDevProperties(properties, reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                /**
                 * @type {SavePointFile}
                 */
                const spf = await openSavePointFileForFile(targetRelaPath);
                spf.setProperties(properties);
                //只写索引文件
                await spf.writeIndexFile();
            }

            reply();
        },


        /**
         * 读取index文件中的properties
         * @param {*} reply 
         */
        async readDevProperties(reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                const spf = await openSavePointFileForFile(targetRelaPath);
                reply(spf.properties || {});
            }

            reply();
        },

        /**
         * 保存从ide端传来的序列化配置,这个后期要融合page setting
         * @param {String} desc 
         *     用户输入的保存点说明,当为"@~~"时,为自动存档
         * @param {String} serializedRootedConfig 
         *     当用户关闭设计webview的时候,这个参数为空
         * @param reply
         */
        async saveDevSavePoint(desc, serializedRootedConfig, reply) {
            /**
             * @type {String}
             */
            let targetRelaPath;
            if (typeof reply === 'string') {
                targetRelaPath = reply;
                //自动存档
                if (desc === '@~~' && !serializedRootedConfig) {
                    let [dirty, _serializedRootedConfig] = immediatelySavePoints.get(targetRelaPath);
                    //在即时保存点是dirty的情况下才需要保存
                    dirty && (serializedRootedConfig = _serializedRootedConfig);
                }
                reply = null;
            } else {
                targetRelaPath = getRelaPathFromWebView(reply);
            }

            let needSave = !serializedRootedConfig ? false : true;

            if (typeof targetRelaPath !== 'string') {
                w.showInformationMessage('mxdev演示不能进行保存!');
                needSave = false;
            }

            if (needSave) {
                //save point handler
                const spf = await openSavePointFileForFile(targetRelaPath);
                /**
                 * 方案体系标识将会作为一个属性体现在settings中,这里先设置为固定值
                 */
                spf.setScheme('v2Element');
                //将时间转为长整型作为时间戳
                const d = new Date();
                await spf.writeSavePoint('' + d.getTime(), desc || '', serializedRootedConfig);
                //返回保存时间
                reply && reply(formatDate(d, 'yyyy/MM/dd hh:mm:ss'));
                //清空相关的临时保存点的dirty状态
                immediatelySavePoints.clearDirty(targetRelaPath);
            }

            //空参数调用响应方法,表示结束
            if (reply && typeof reply === 'function') {
                reply();
            }

            return needSave;
        },

        /**
         * 清理保存点历史,保留最近的2个快速存档,用户自定义要删除的普通存档
         */
        async clearDevSavePoint(reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                const spf = await openSavePointFileForFile(targetRelaPath);
                /**
                 * 普通和快速保存点各自保留的数量
                 */
                const retainCommon = 6, retainFast = 3;
                const selectdTimes = await this._selectAndConfirmTimes(false, spf, '选择要删除的保存点', true, (items) => {
                    let commonCounnt = 0, fastCount = 0;
                    items.forEach(item => {
                        const isFastSave = item.description === '快速存档';
                        const setPick = isFastSave ? (++fastCount > retainFast) : (++commonCounnt > retainCommon);
                        /**
                         * {@link vscode.QuickPickItem}
                         */
                        setPick && (item.picked = true);
                    });
                });

                if (selectdTimes && selectdTimes.length) {
                    const isDeletedIndex = await spf.deleteSavePoint(selectdTimes);
                    info(isDeletedIndex);
                    await spf.clearDeleted();
                    w.showInformationMessage(`"${targetRelaPath}"的保存点文件已经成功清理`);
                }

            }

            reply();
        },

        /**
         * ide端在config改变时,会自动延迟推送实时的保存点存档,
         * @param {String} serializedRootedConfig 
         * @param {*} reply 
         */
        updateImmediatelySavePoint(serializedRootedConfig, reply) {
            const targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            if (targetRelaPath) {
                immediatelySavePoints.set(targetRelaPath, serializedRootedConfig);
                info('immediatelySavePoints', targetRelaPath, serializedRootedConfig.length);
            }
            reply();
        },

        /**
         * 选择或通过quickPick和用户交互选择,以确定时间点
         * @param {Boolean} mostRecently 
         * @param {SavePointFile} spf 
         * @param {String} placeHolder
         * @param {Boolean} canPickMany
         * @param {Function} callback
         */
        async _selectAndConfirmTimes(mostRecently, spf, placeHolder, canPickMany, callback = null) {
            const times = Object.keys(spf.indexes);
            if (times && times.length) {
                const items = [];
                /**
                 * 倒序,时间上按照从大到小的顺序
                 */
                times.sort().reverse();
                if (mostRecently) {
                    //取第一个时间,也就是最近的时间
                    return times.shift();
                } else {
                    const _map = {};
                    times.sort().reverse().forEach(time => {
                        // @ts-ignore
                        const timeStr = formatDate(new Date(time * 1), 'yyyy/MM/dd hh:mm:ss');
                        _map[timeStr] = time;
                        const desc = spf.indexes[time].desc;
                        items.push({ label: timeStr, description: desc === '@~~' ? '快速存档' : desc });
                    });

                    callback && callback(items);
                    /**
                     * {@link vscode.QuickPickOptions}
                     */
                    const chosen = await w.showQuickPick(items, {
                        placeHolder, canPickMany, ignoreFocusOut: false
                    });

                    function _getTargetTime(c) {
                        return _map[c.label];
                    }

                    if (chosen) {
                        if (canPickMany) {
                            const res = [];
                            [].concat(chosen).forEach(c => {
                                const targetTime = _getTargetTime(c);
                                targetTime && res.push(targetTime);
                            });
                            return res;
                        } else {
                            return _getTargetTime(chosen);
                        }
                    }
                }
            }
        },

        /**
         * 加载save point
         * @param {boolean} mostRecently
         *   是否取时间上最近一次的保存点数据
         * @param targetRelaPath
         *   测试路径,用于测试
         * @param {*} reply 
         */
        async loadDevSavePoint(mostRecently, targetRelaPath, reply) {
            if (typeof targetRelaPath === 'function') {
                reply = targetRelaPath;
                targetRelaPath = getRelaPathFromWebViewNotDemo(reply);
            }

            if (targetRelaPath) {
                const spf = await openSavePointFileForFile(targetRelaPath);
                const targetTime = await this._selectAndConfirmTimes(mostRecently, spf, '请选择一个时间点进行还原:', false);
                if (targetTime) {
                    info('will read savepoint on:', targetTime)
                    const config = await spf.readSavePoint(targetTime);
                    //保持和immediatelySavePoints同步
                    immediatelySavePoints.init(targetRelaPath, config);
                    info('savepoint config readed:', targetRelaPath, config.length)
                    reply({ time: formatDate(new Date(targetTime * 1), 'yyyy/MM/dd hh:mm:ss'), config });
                }
            }

            //空参数调用响应方法,表示结束
            reply();
        },
    };

    /**
     * webview关闭或崩溃时自动进行存档
     */
    panelHandler.addListener('change', async (type, param) => {
        if (type === 'destroy') {
            const targetRelaPath = param;
            await spfService.saveDevSavePoint('@~~', null, targetRelaPath)
                && w.showInformationMessage(`设计页面：${targetRelaPath}已经关闭，已经自动为您进行存档`);
        }
    });


    serviceHandler.registerService(spfService);
}


module.exports = { getSavePointFilePath, openSavePointFile, openSavePointFileForFile, contributeServicesForSavePoint }